package com.xiyuan.smartutils;

import com.xiyuan.smartutils.client.http.HttpGetStream;
import com.xiyuan.smartutils.client.http.HttpResult;
import com.xiyuan.smartutils.constants.CodeConstants;
import com.xiyuan.smartutils.exceptions.UtilException;
import com.xiyuan.smartutils.reflect.Unsafe;

import java.io.UnsupportedEncodingException;
import java.net.*;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

/**
 * URL相关工具类
 *
 * @version v1.0.0 @author lgz 2017-8-27 新建与整理
 */

public final class Urls implements CodeConstants {
    static {
        Unsafe.init();
    }
    private Urls() {}
    
    /**
     * 从URL对象中获取不被编码的路径Path<br>
     * 对于本地路径，URL对象的getPath方法对于包含中文或空格时会被编码，导致本读路径读取错误。<br>
     * 此方法将URL转为URI后获取路径用于解决路径被编码的问题
     *
     * @param url {@link URL}
     * @return 路径
     * 
     */
    public static String getDecodedPath(URL url) {
        if (null == url) {
            return null;
        }
        
        String path = null;
        try {
            // URL对象的getPath方法对于包含中文或空格的问题
            path = toURI(url).getPath();
        } catch (UtilException e) {
            // ignore
        }
        return (null != path) ? path : url.getPath();
    }
    /**
     * 转URL为URI
     *
     * @param url URL
     * @return URI
     * @throws UtilException 包装URISyntaxException
     */
    public static URI toURI(URL url) throws UtilException {
        return toURI(url, false);
    }
    
    /**
     * 转URL为URI
     *
     * @param url      URL
     * @param isEncode 是否编码参数中的特殊字符（默认UTF-8编码）
     * @return URI
     * @throws UtilException 包装URISyntaxException
     * 
     */
    public static URI toURI(URL url, boolean isEncode) throws UtilException {
        if (null == url) {
            return null;
        }
        
        return toURI(url.toString(), isEncode);
    }
    
    /**
     * 转字符串为URI
     *
     * @param location 字符串路径
     * @return URI
     * @throws UtilException 包装URISyntaxException
     */
    public static URI toURI(String location) throws UtilException {
        return toURI(location, false);
    }
    
    /**
     * 转字符串为URI
     *
     * @param location 字符串路径
     * @param isEncode 是否编码参数中的特殊字符（默认UTF-8编码）
     * @return URI
     * @throws UtilException 包装URISyntaxException
     * 
     */
    public static URI toURI(String location, boolean isEncode) throws UtilException {
        if (isEncode) {
            location = encode(location, "UTF-8");
        }
        try {
            return new URI(StrUtils.trim(location));
        } catch (URISyntaxException e) {
            throw new UtilException(e);
        }
    }
    /**
     * URL 按UTF-8编码
     *
     * @param src 原字符串
     * @return 结果字符串
     */
    public static String encodeUTF8(String src) {
        return encode(src, _UTF_8_);
    }
    
    /**
     * URL编码
     *
     * @param src      原字符串
     * @param encoding 指定编码
     * @return 编码后的值
     */
    public static String encode(String src, String encoding) {
        if (Validates.isEmpty(src)) {
            return src;
        }
        
        try {
            return URLEncoder.encode(src, encoding);
        }
        catch (UnsupportedEncodingException e) {
            throw Asserts.exception("非法的字符编码[" + encoding + "]");
        }
    }
    
    /**
     * URL 按UTF-8解码
     *
     * @param src 原字符串
     * @return 结果字符串
     */
    public static String decodeUTF8(String src) {
        return decode(src, _UTF_8_);
    }
    
    /**
     * URL解码
     *
     * @param src      原字符串
     * @param encoding 指定编码
     * @return 编码后的值
     */
    public static String decode(String src, String encoding) {
        if (Validates.isEmpty(src)) {
            return src;
        }
        
        try {
            return URLDecoder.decode(src, encoding);
        }
        catch (UnsupportedEncodingException e) {
            throw Asserts.exception("非法的字符编码[" + encoding + "]");
        }
    }
    
    /**
     * 根据URL，默认以UTF-8生成KEY和VALUE放置到指定的哈希表
     *
     * @param url URL
     * @param map 指定哈希表
     */
    public static void toMapUTF8(String url, Map<String, String> map) {
        toMap(url, map, _UTF_8_);
    }
    
    /**
     * 根据URL，生成KEY和VALUE放置到指定的哈希表
     *
     * @param url      URL
     * @param map      指定哈希表
     * @param encoding 指定编码格式
     */
    public static void toMap(String url, Map<String, String> map, String encoding) {
        if (map == null) {
            return;
        }
        
        map.putAll(toMap(url, encoding));
    }
    
    /**
     * 根据URL，默认以UTF-8生成KEY和VALUE对应哈希表
     *
     * @param url URL
     * @return 生成KEY和VALUE对应哈希表
     */
    public static Map<String, String> toMapUTF8(String url) {
        return toMap(url, _UTF_8_);
    }
    
    /**
     * 根据URL，生成KEY和VALUE对应哈希表
     *
     * @param url      URL
     * @param encoding 编码格式
     * @return 生成KEY和VALUE对应LinkedHashMap表
     */
    public static Map<String, String> toMap(String url, String encoding) {
        Map<String, String> map = new LinkedHashMap<String, String>();
        if (Validates.isEmpty(url)) {
            return map;
        }
        url = StrUtils.trimLeft(url, "?");
        url = StrUtils.trimLeft(url, "&");
        int i0 = url.indexOf('?');
        if (i0 != -1) {
            url = url.substring(i0 + 1);
        }
        
        int i = -1;
        while ((i = url.indexOf('&')) != -1) {
            String keyValue = url.substring(0, i);
            int i1 = keyValue.indexOf('=');
            if (i1 == -1) {
                if (Validates.isBlank(keyValue)) {
                    continue;
                }
                
                map.put(keyValue, null);
            }
            else {
                String key = keyValue.substring(0, i1);
                String value = keyValue.substring(i1 + 1);
                value = decode(value, encoding);
                String mapValue = map.get(key);
                if (mapValue != null) {
                    value = mapValue + "," + value;
                }
                map.put(key, value);
            }
            
            url = url.substring(i + 1);
        }
        
        // 还有最后一个
        int i1 = url.indexOf('=');
        if (i1 != -1) {
            String key = url.substring(0, i1);
            String value = url.substring(i1 + 1);
            value = decode(value, encoding);
            String mapValue = map.get(key);
            if (mapValue != null) {
                value = mapValue + "," + value;
            }
            map.put(key, value);
        }
        
        return map;
    }
    
    /**
     * 根据URL，生成KEY和VALUE对应哈希表，该URL没有作编码处理
     *
     * @param url URL
     * @return 生成KEY和VALUE对应哈希表
     */
    public static Map<String, String> toMapNoEncoded(String url) {
        Map<String, String> map = new HashMap<String, String>();
        if (Validates.isEmpty(url)) {
            return map;
        }
        
        int i0 = url.indexOf('?');
        if (i0 != -1) {
            url = url.substring(i0 + 1);
        }
        
        int i = url.indexOf('&');
        while (i != -1) {
            String keyValue = url.substring(0, i);
            int i1 = keyValue.indexOf('=');
            if (i1 == -1) {
                map.put(keyValue, null);
            }
            else {
                String key = keyValue.substring(0, i1);
                String value = keyValue.substring(i1 + 1);
                String mapValue = map.get(key);
                if (mapValue != null) {
                    value = mapValue + "," + value;
                }
                map.put(key, value);
            }
            
            url = url.substring(i + 1);
            i = url.indexOf('&');
        }
        
        // 还有最后一个
        int i1 = url.indexOf('=');
        if (i1 != -1) {
            String key = url.substring(0, i1);
            String value = url.substring(i1 + 1);
            String mapValue = map.get(key);
            if (mapValue != null) {
                value = mapValue + "," + value;
            }
            map.put(key, value);
        }
        
        return map;
    }
    
    /**
     * 提供两个参数拼接成字符串，不作URLEcoding，用于对参数作签名验签用
     *
     * @param key1   键1
     * @param value1 值1
     * @param key2   键2
     * @param value2 值2
     * @return 拼接后字符串
     */
    public static String toSignString(String key1, Object value1, String key2, Object value2) {
        TreeMap<String, Object> treeParamMap = new TreeMap<String, Object>();
        treeParamMap.put(key1, value1);
        treeParamMap.put(key2, value2);
        return toSign(treeParamMap);
    }
    
    /**
     * 提供三个参数拼接成字符串，不作URLEcoding，用于对参数作签名验签用
     *
     * @param key1   键1
     * @param value1 值1
     * @param key2   键2
     * @param value2 值2
     * @param key3   键3
     * @param value3 值3
     * @return 拼接后字符串
     */
    public static String toSignString(String key1,
                                      Object value1,
                                      String key2,
                                      Object value2,
                                      String key3,
                                      Object value3) {
        TreeMap<String, Object> treeParamMap = new TreeMap<String, Object>();
        treeParamMap.put(key1, value1);
        treeParamMap.put(key2, value2);
        treeParamMap.put(key3, value3);
        return toSign(treeParamMap);
    }
    
    /**
     * 提供四个参数拼接成字符串，不作URLEcoding，用于对参数作签名验签用
     *
     * @param key1   键1
     * @param value1 值1
     * @param key2   键2
     * @param value2 值2
     * @param key3   键3
     * @param value3 值3
     * @param key4   键4
     * @param value4 值4
     * @return 拼接后字符串
     */
    public static String toSignString(String key1,
                                      Object value1,
                                      String key2,
                                      Object value2,
                                      String key3,
                                      Object value3,
                                      String key4,
                                      Object value4) {
        TreeMap<String, Object> treeParamMap = new TreeMap<String, Object>();
        treeParamMap.put(key1, value1);
        treeParamMap.put(key2, value2);
        treeParamMap.put(key3, value3);
        treeParamMap.put(key4, value4);
        return toSign(treeParamMap);
    }
    
    /**
     * 把数组所有元素排序拼接成字符串，不作URLEcoding，并按照"参数=参数值"的模式用"&"字符拼接成字符串，用于对参数作签名验签用
     *
     * @param paramMap TreeMap
     * @return 拼接后字符串
     */
    public static String toSign(TreeMap<String, Object> paramMap) {
        if (paramMap.isEmpty()) {
            return _EMPTY_;
        }
        
        paramMap = new TreeMap<String, Object>(paramMap);
        StringBuilder strb = new StringBuilder();
        for (Entry<String, Object> entry : paramMap.entrySet()) {
            strb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        strb.setLength(strb.length() - 1);
        return strb.toString();
    }
    
    /**
     * 提供两个参数拼接成字符串，作URLEncoding
     *
     * @param key1   键1
     * @param value1 值1
     * @param key2   键2
     * @param value2 值2
     * @return 拼接后字符串
     */
    public static String toQueryString(String key1,
                                       Object value1,
                                       String key2,
                                       Object value2) {
        TreeMap<String, Object> treeMap = new TreeMap<String, Object>();
        treeMap.put(key1, value1);
        treeMap.put(key2, value2);
        return toQueryString(treeMap);
    }
    
    /**
     * 提供三个参数拼接成字符串，作URLEncoding
     *
     * @param key1   键1
     * @param value1 值1
     * @param key2   键2
     * @param value2 值2
     * @param key3   键3
     * @param value3 值3
     * @return 拼接后字符串
     */
    public static String toQueryString(String key1,
                                       Object value1,
                                       String key2,
                                       Object value2,
                                       String key3,
                                       Object value3) {
        TreeMap<String, Object> treeMap = new TreeMap<String, Object>();
        treeMap.put(key1, value1);
        treeMap.put(key2, value2);
        treeMap.put(key3, value3);
        return toQueryString(treeMap);
    }
    
    /**
     * 提供四个参数拼接成字符串，作URLEncoding
     *
     * @param key1   键1
     * @param value1 值1
     * @param key2   键2
     * @param value2 值2
     * @param key3   键3
     * @param value3 值3
     * @param key4   键4
     * @param value4 值4
     * @return 拼接后字符串
     */
    public static String toQueryString(String key1,
                                       Object value1,
                                       String key2,
                                       Object value2,
                                       String key3,
                                       Object value3,
                                       String key4,
                                       Object value4) {
        TreeMap<String, Object> treeParamMap = new TreeMap<String, Object>();
        treeParamMap.put(key1, value1);
        treeParamMap.put(key2, value2);
        treeParamMap.put(key3, value3);
        treeParamMap.put(key4, value4);
        return toQueryString(treeParamMap);
    }
    
    /**
     * 把数组所有元素排序，作URLEncoding
     *
     * @param paramMap 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     */
    public static String toQueryString(Map<String, ?> paramMap) {
        StringBuilder strb = new StringBuilder();
        for (Entry<String, ?> entry : paramMap.entrySet()) {
            if (strb.length() > 0) {
                strb.append("&");
            }
            
            String key = encodeUTF8(entry.getKey());
            String value = encodeUTF8(String.valueOf(entry.getValue()));
            strb.append(key).append("=").append(value);
        }
        
        return strb.toString();
    }
    
    /**
     * 给定一个url,和key,取得value
     *
     * @param url URL
     * @param key 键值
     * @return 给定一个url, 和key, 取得value
     */
    public static String get(String url, String key) {
        if (Validates.isEmpty(url) || Validates.isEmpty(key)) {return _EMPTY_;}
        
        // 统一格式，URL="&aid=fdd&id=ood&idd=dd"，KEY="&id="
        key = "&" + key + "=";
        int p = url.indexOf("?");
        if (p != -1) {
            url = url.substring(p + 1);
        }
        url = StrUtils.addStartsWith(url, "&");
        
        int ind = url.indexOf(key);
        if (ind == -1) {return _EMPTY_;}
        
        int start = ind + key.length();
        int ind2 = url.indexOf('&', start);
        if (ind2 == -1) {
            return url.substring(start);
        }
        else {
            return url.substring(start, ind2);
        }
    }
    
    /**
     * 给定一个url,和key,取得value
     *
     * @param url      URL
     * @param key      键值
     * @param encoding 编码
     * @return 给定一个url, 和key, 取得value
     */
    public static String get(String url, String key, String encoding) {
        String value = get(url, key);
        return decode(value, encoding);
    }
    
    /**
     * 给定一组key,value增加到url中
     *
     * @param url   URL
     * @param key   键值
     * @param value 值
     * @return 增加后的url
     */
    public static String add(String url, String key, String value) {
        if (Validates.isBlank(url) || Validates.isBlank(key)) {
            return StrUtils.trim(url);
        }
        
        if (Validates.isBlank(value)) {value = _EMPTY_;}
        url = url.trim();
        url += (url.indexOf("?") == -1 ? "?" : "&") + key.trim() + "=" + value.trim();
        return url;
    }
    
    /**
     * 给定一组key,value增加到url中
     *
     * @param url      URL
     * @param key      键值
     * @param value    值
     * @param encoding 编码
     * @return 增加后的url
     */
    public static String add(String url, String key, String value, String encoding) {
        value = encode(value, encoding);
        return add(url, key, value);
    }
    
    /**
     * 给定一组key,value修改到url中,注：该方法仅修改第一个，多个不处理
     *
     * @param url   URL
     * @param key   键值
     * @param value 值
     * @return 修改后的url, 注：该方法仅修改第一个，多个不处理
     */
    public static String modify(String url, String key, String value) {
        if (Validates.isEmpty(url) || Validates.isEmpty(key)) {
            return url;
        }
        
        if (Validates.isEmpty(value)) {
            value = _EMPTY_;
        }
        
        int index = url.indexOf(key + "=");
        if (index == -1) {return add(url, key, value);}
        
        if (index + key.length() == url.length()) {return url += value;}
        
        int valueIndex = url.indexOf('&', index + key.length());
        String substring = url.substring(0, index + key.length() + 1);
        if (valueIndex == -1) {return substring + value;}
        
        return substring + value + url.substring(valueIndex);
    }
    
    /**
     * 给定一组key,value修改到url中,注：该方法仅修改第一个，多个不处理
     *
     * @param url      URL
     * @param key      键值
     * @param value    值
     * @param encoding 编码
     * @return 修改后的url, 注：该方法仅修改第一个，多个不处理
     */
    public static String modify(String url, String key, String value, String encoding) {
        value = encode(value, encoding);
        return modify(url, key, value);
    }
    
    /**
     * 给定一组key删除到url中,注：该方法仅删除第一个，多个不处理
     *
     * @param url URL
     * @param key 键值
     * @return 删除后的url, 注：该方法仅删除第一个，多个不处理
     */
    public static String delete(String url, String key) {
        if (Validates.isBlank(url) || Validates.isBlank(key)) {
            return StrUtils.trim(url);
        }
        
        url = url.trim();
        key = key.trim();
        int index = url.indexOf(key + "=");
        if (index == -1) {
            return url;
        }
        
        // 如果是最后一个，则该KEY的VALUE为空,或最后一个KEY,则删除前一个连接符
        if (index + key.length() == url.length()) {
            return url.substring(0, index);
        }
        
        int valueIndex = url.indexOf('&', index + key.length());
        if (valueIndex == -1) {
            return url.substring(0, index - 1);
        }
        
        // 如果不是最后一个,则删除后一个连接符
        return url.substring(0, index) + url.substring(valueIndex + 1);
    }
    
    /**
     * 读取URL内容
     *
     * @param url         指定URL
     * @param connTimeout 连接超时时长，单位：秒
     * @param readTimeout 读流超时时长，单位：秒
     * @return 内容字节数组
     */
    public static HttpResult read(String url, int connTimeout, int readTimeout) {
        HttpGetStream conn = new HttpGetStream(url);
        conn.setConnectTimeout(connTimeout);
        conn.setReadTimeout(readTimeout);
        conn.execute();
        
        if (!conn.isOK()) {
            return new HttpResult(conn.getResponseStatus(), conn.getResponseText());
        }
        
        return new HttpResult(conn.getResponseStatus(), conn.getResponseText(), conn.getBytes());
    }
    
    /**
     * 读取URL内容
     *
     * @param url     指定URL
     * @param timeout 连接和读流超时时长，单位：秒
     * @return 内容字节数组
     */
    public static HttpResult read(String url, int timeout) {
        return read(url, timeout, timeout);
    }
}
